/**
 * e3db Fast database library for Grails
 * Copyright (C) 2009-2010 Collegeman.net, LLC
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package net.collegeman.grails.e3db;

import java.util.*;
import java.sql.*;
import javax.sql.*;
import groovy.lang.*;
import java.util.regex.*;
import org.apache.log4j.*;
import org.springframework.context.ApplicationContext;
import org.springframework.util.StringUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.jdbc.core.simple.*;
import org.springframework.jdbc.datasource.*;

public class Template extends SqlBuffer {
	
	private static final Pattern multiLineWhitespace = Pattern.compile("[\n\r\t]");
	private static final Pattern extraSpaces = Pattern.compile(" +");
	private static final String removeWhitespace(String string) {
		string = multiLineWhitespace.matcher(string).replaceAll(" ");
		return extraSpaces.matcher(string).replaceAll(" ");
	}
	
	protected static final String METHOD_ROW_SET = "rowSet";
	protected static final String METHOD_LIST = "list";
	protected static final String METHOD_OBJECT = "object";
	
	protected DataSource dataSource;
	protected SimpleJdbcTemplate simpleJdbcTemplate;
	
	private static final Logger log = Logger.getLogger(Template.class);
	
	// an instance of Query is defined in plugin doWithSpring to grab app context and default data source
	
	protected static ApplicationContext ctx;
	protected static DataSource defaultDataSource;
	protected static SimpleJdbcTemplate defaultSimpleJdbcTemplate;
	protected static final void setApplicationContext(ApplicationContext applicationContext) {
		ctx = applicationContext;
		
		// first, go for grails default "dataSource"
		try {
			defaultDataSource = (DataSource) ctx.getBean("dataSource");
			defaultSimpleJdbcTemplate = new SimpleJdbcTemplate(defaultDataSource);
		} catch (NoSuchBeanDefinitionException e) {
			// on to the last phase
		} finally {
			// TODO: implement looking for the first DataSource in bean scope
		}
	}
	
	public static void setDefaultDataSource(DataSource dataSource) {
		defaultDataSource = dataSource;
		defaultSimpleJdbcTemplate = new SimpleJdbcTemplate(defaultDataSource);
	}
	
	protected SimpleJdbcTemplate getSJT() {
		if (simpleJdbcTemplate != null)
			return simpleJdbcTemplate;
		else if (defaultSimpleJdbcTemplate != null)
			return defaultSimpleJdbcTemplate;
		else
			throw new IllegalStateException("DataSource for E3DB Template has not be initialized.");
	}
	
	protected final static <T> T query(String method, Closure closure, Class<T> requiredType) {
		Template t = new Template();
		closure.setDelegate(t);
		closure.setResolveStrategy(Closure.DELEGATE_FIRST);

		Object sql = closure.call();
		if (hasLength(sql))
			t.sql(String.valueOf(sql));
		
		return (T) query(method, t.getSql(), requiredType, t.getQueryParams());
	}
	
	protected final static <T> T query(String method, String sql, Class<T> requiredType, Object ... args) {
		if (log.isDebugEnabled()) {
			log.debug(removeWhitespace(sql) + ": " + Arrays.asList(args));
		}
		
		if (args != null && args.length > 0) {
			if (args[0] instanceof Map) {
				Map map = (Map) args[0];
				
				if (METHOD_LIST.equals(method)) {
					return (T) new Template().getSJT().queryForList(sql, map);
				}
				else if (METHOD_OBJECT.equals(method)) {
					return (T) new Template().getSJT().queryForObject(sql, requiredType, map);
				}
				else if (METHOD_ROW_SET.equals(method)) {
					throw new IllegalStateException("rows() does not support named parameters. You must use listed parameters instead, e.g., rows('SELECT * FROM ...', param1, param2, ...) or rows { sql('SELECT * FROM ...', param1, param2, ...) }.");
				}
				else {
					throw new IllegalStateException("Unrecognized query method: " + method);
				}
			}
			else {
				if (args[0] instanceof List) {
					args = ((List) args[0]).toArray();
				}
				
				if (METHOD_LIST.equals(method)) {
					return (T) new Template().getSJT().queryForList(sql, args);
				}
				else if (METHOD_OBJECT.equals(method)) {
					return (T) new Template().getSJT().queryForObject(sql, requiredType, args);
				}
				else if (METHOD_ROW_SET.equals(method)) {
					return (T) new Template().getSJT().getJdbcOperations().queryForRowSet(sql, args);
				}
				else {
					throw new IllegalStateException("Unrecognized query method: " + method);
				}
			}
		}
		else {
			if (METHOD_LIST.equals(method)) {
				return (T) new Template().getSJT().queryForList(sql);
			}
			else if (METHOD_OBJECT.equals(method)) {
				return (T) new Template().getSJT().queryForObject(sql, requiredType);
			}
			else if (METHOD_ROW_SET.equals(method)) {
				return (T) new Template().getSJT().getJdbcOperations().queryForRowSet(sql);
			}
			else {
				throw new IllegalStateException("Unrecognized query method: " + method);
			}
		}
	}
	
	public final static int exec(Closure closure) {
		Template t = new Template(){};
		closure.setDelegate(t);
		closure.setResolveStrategy(Closure.DELEGATE_FIRST);

		Object sql = closure.call();
		if (hasLength(sql))
			t.sql(String.valueOf(sql));
			
		return exec(t.getSql(), t.getQueryParams());
	}
	
	public final static int exec(String sql, Object ... args) {
		if (log.isDebugEnabled()) {
			log.debug(removeWhitespace(sql) + ": " + Arrays.asList(args));
		}
		
		return new Template(){}.getSJT().update(sql, args);
	}
	
	
	/**
	 * @param name The name of a dataSource bean in Application context.
	 */
	public final void dataSource(String name) {
		if (ctx == null)
			throw new IllegalStateException("ApplicationContext not initialized.");
		dataSource = (DataSource) ctx.getBean(name);	
		simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);		
	}
	
	/**
	 * @param dataSource A JDBC DataSource implementation
	 */
	public final void dataSource(DataSource dataSource) {
		if (dataSource == null)
			throw new IllegalArgumentException("dataSource cannot be null.");
		this.dataSource = dataSource;
		simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);		
	}
	
	public final void dataSource(Map<String, Object> config) {
		Driver driverInstance = null;
		Object driver = config.get("driver");
		
		try {
			if (driver == null)
				throw new IllegalArgumentException("driver configuration parameter cannot be null.");
	
			if (driver instanceof Class) {
				driverInstance = (Driver) ((Class) driver).newInstance();
			}
			else if (driver instanceof String) {
				driverInstance = (Driver) Class.forName((String) driver).newInstance();
			}
			else if (driver instanceof Driver) {
				driverInstance = (Driver) driver;
			}
			else {
				throw new IllegalArgumentException("driver is unrecognized type: " + driver.getClass());
			}
		} catch (InstantiationException e) {
			throw new RuntimeException(e);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		}
		
		dataSource(new SimpleDriverDataSource(
			driverInstance,
			String.valueOf(config.get("url")),
			String.valueOf(config.get("username")),
			String.valueOf(config.get("password"))
		));
	}
	
	/**
	 * Convenience handler for misspellings of dataSource method.
	 */
	public final void datasource(String name) {
		dataSource(name);
	}
	
	public static final SqlBuffer buffer(Closure closure) {
		SqlBuffer buf = new SqlBuffer();
		closure.setDelegate(buf);
		closure.setResolveStrategy(Closure.DELEGATE_FIRST);
		closure.call();
		return buf;
	}
	

}